/**
 *    Copyright (C) 2011 Nadim Benabdenbi <nadim.benabdenbi@gmail.com>
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.google.code.maven.plugin.beans.builder;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;

import com.sun.codemodel.ClassType;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import com.sun.codemodel.JTypeVar;
import com.sun.codemodel.JVar;

/**
 * 
 * {@link BuilderSourceGenerator} implementation: Generate builder source using reflection on the bean type.
 * 
 * @author <a href="mailto:nadim.benabdenbi@gmail.com">Nadim Benabdenbi</a>
 * @version 1.0
 * @since JDK1.6
 * 
 * @see BuilderSourceGenerator
 * @param T
 *            the bean reflection type
 * @param M
 *            the method type
 * @param C
 *            the constructor type
 */
public abstract class AbstractBuilderSourceGenerator<T, M, C> implements BuilderSourceGenerator {
	
	protected static final String GETTER_METHOD_PREFIX = "get";
	protected static final String SETTER_METHOD_PREFIX = "set";
	protected static final String FLUENT_SETTER_METHOD_PREFIX = "with";
	protected static final String FLUENT_SETTER_PARAMETER_NAME = "value";
	protected static final int SETTER_METHOD_PREFIX_LEN = SETTER_METHOD_PREFIX.length();
	
	protected final JClass listImpl;
	protected final JClass setImpl;
	protected final JClass mapImpl;
	
	protected final JClass list;
	protected final JClass set;
	protected final JClass map;
	/**
	 * the code model
	 */
	protected final JCodeModel codeModel;
	
	/**
	 * the underlying builder bean java class
	 */
	protected final JClass beanClass;
	
	/**
	 * the builder class
	 */
	protected final JDefinedClass builderClass;
	
	/**
	 * the builder bean instance.
	 */
	protected final JFieldVar beanInstance;
	
	protected final List<String> generics;
	/**
	 * holds setter methods as keys and generated fields as values.
	 */
	protected final Map<M, JFieldVar> collectionFields;
	
	protected AbstractBuilderSourceGenerator(JCodeModel codeModel, T t) throws JClassAlreadyExistsException {
		super();
		this.codeModel = codeModel;
		this.beanClass = buildBeanClass(codeModel, t);
		builderClass = beanClass._package()._class(JMod.PUBLIC, beanClass.name() + "Builder", ClassType.CLASS);
		JTypeVar[] typeParams = beanClass.typeParams();
		generics = new ArrayList<String>();
		for (int i = 0; i < typeParams.length; i++) {
			generics.add(typeParams[i].name());
			builderClass.generify(typeParams[i].name(), typeParams[i]._extends());
		}
		builderClass.javadoc().add(beanClass.name() + "Builder");
		beanInstance = builderClass.field(JMod.PROTECTED, beanClass, "instance");
		collectionFields = new HashMap<M, JFieldVar>();
		listImpl = codeModel.ref(ArrayList.class);
		setImpl = codeModel.ref(HashSet.class);
		mapImpl = codeModel.ref(HashMap.class);
		list = codeModel.ref(List.class);
		set = codeModel.ref(Set.class);
		map = codeModel.ref(Map.class);
	}
	
	protected abstract JClass buildBeanClass(JCodeModel codeModel, T t);
	
	/**
	 * 
	 * @param javaType
	 * @return
	 */
	protected JType convert(Class<?> javaType) {
		return codeModel._ref(javaType);
	}
	
	/**
	 * gets the package from the code model when exists otherwise creates this package over this code model
	 * 
	 * @param codeModel
	 *            the code model
	 * @param packageName
	 *            the package name
	 * @return
	 */
	protected static final JPackage getPackage(JCodeModel codeModel, String packageName) {
		Iterator<JPackage> packages = codeModel.packages();
		JPackage jPackage;
		while (packages.hasNext()) {
			jPackage = packages.next();
			if (jPackage.name().equals(packageName)) {
				return jPackage;
			}
		}
		return codeModel._package(ClassUtils.getPackageName(packageName));
	}
	
	protected abstract List<C> getConstructors();
	
	protected abstract List<JType> getConstructorParameters(C constructor);
	
	protected abstract List<JClass> getConstructorExceptions(C constructor);
	
	protected abstract boolean isConstructorPublic(C constructor);
	
	/**
	 * generates builder constructors from the builder underlying type
	 */
	protected void generateBuilderConstructors() {
		// builder constructor
		for (C constructor : getConstructors()) {
			if (isConstructorPublic(constructor)) {
				generateBuilderConstructor(getConstructorExceptions(constructor), getConstructorParameters(constructor));
			}
		}
	}
	
	protected void generateBuilderConstructor(List<JClass> exceptions, List<JType> parameterTypes) {
		JMethod constructor = builderClass.constructor(JMod.PUBLIC);
		for (JClass exception : exceptions) {
			constructor._throws(exception);
		}
		JBlock body = constructor.body();
		JInvocation newInstance = JExpr._new(beanClass);
		if (parameterTypes.size() > 1) {
			int paramCount = 0;
			for (JType parameterType : parameterTypes) {
				JVar param = constructor.param(parameterType, FLUENT_SETTER_PARAMETER_NAME + paramCount++);
				newInstance.arg(param);
			}
		} else if (parameterTypes.size() == 1) {
			JVar param = constructor.param(parameterTypes.get(0), FLUENT_SETTER_PARAMETER_NAME);
			newInstance.arg(param);
		}
		body.assign(beanInstance, newInstance);
	}
	
	protected void generateBuilderDefaultConstructor() {
		// default builder constructor from a bean instance
		JMethod defaultConstructor = builderClass.constructor(JMod.PUBLIC);
		JVar defaultConstructorParameter = defaultConstructor.param(beanClass, FLUENT_SETTER_PARAMETER_NAME);
		JBlock defaultConstructorBody = defaultConstructor.body();
		defaultConstructorBody._if(defaultConstructorParameter.eq(JExpr._null()))._then()._throw(JExpr._new(codeModel.ref(NullPointerException.class)));
		defaultConstructorBody.assign(beanInstance, defaultConstructorParameter);
	}
	
	protected void generateBuildMethod() {
		JMethod buildMethod = builderClass.method(JMod.PUBLIC, beanInstance.type(), "build");
		// handle collections fields
		for (M method : collectionFields.keySet()) {
			buildMethod.body()._if(collectionFields.get(method).ne(JExpr._null()))//
					._then().invoke(beanInstance, getMethodName(method)).arg(collectionFields.get(method));
			// add setters exception
			for (JClass exception : getMethodExceptions(method)) {
				buildMethod._throws(exception);
			}
		}
		buildMethod.body()._return(beanInstance);
	}
	
	protected abstract String getMethodName(M method);
	
	protected abstract JType getMethodParameter(M method);
	
	protected abstract List<JClass> getMethodExceptions(M method);
	
	protected void generateObjectFluentMethod(M method) {
		JMethod fluentMethod = builderClass.method(JMod.PUBLIC, builderClass, FLUENT_SETTER_METHOD_PREFIX
				+ getMethodName(method).substring(SETTER_METHOD_PREFIX_LEN));
		JVar parameter = fluentMethod.param(getMethodParameter(method), FLUENT_SETTER_PARAMETER_NAME);
		// add setter exceptions
		for (JClass exception : getMethodExceptions(method)) {
			fluentMethod._throws(exception);
		}
		JBlock body = fluentMethod.body();
		body.invoke(beanInstance, getMethodName(method)).arg(parameter);
		body._return(JExpr._this());
	}
	
	protected abstract JClass getCollectionGeneric(M method);
	
	protected void generateCollectionFluentMethod(M method, JClass collectionDefaulClass) {
		JClass collectionGeneric = getCollectionGeneric(method);
		// check if the list is a generic one
		if (collectionGeneric != null) {
			JClass collectionClass = ((JClass) getMethodParameter(method)).narrow(collectionGeneric);
			// builder list field definition
			JFieldVar jList = builderClass.field(JMod.PRIVATE, collectionClass, StringUtils.uncapitalize(getMethodName(method).substring(
					SETTER_METHOD_PREFIX_LEN)));
			collectionFields.put(method, jList);
			// item method generation
			JMethod fluentListItemMethod = builderClass.method(JMod.PUBLIC, builderClass, FLUENT_SETTER_METHOD_PREFIX
					+ getMethodName(method).substring(SETTER_METHOD_PREFIX_LEN));
			JVar fluentListItemMethodParameter = fluentListItemMethod.param(collectionGeneric, FLUENT_SETTER_PARAMETER_NAME);
			JBlock fluentListItemMethodBody = fluentListItemMethod.body();
			fluentListItemMethodBody._if(jList.eq(JExpr._null()))._then().assign(jList, JExpr._new(collectionDefaulClass.narrow(collectionGeneric)));
			fluentListItemMethodBody.invoke(jList, "add").arg(fluentListItemMethodParameter);
			fluentListItemMethodBody._return(JExpr._this());
			// items method generation
			JMethod fluentItemsMethod = builderClass.method(JMod.PUBLIC, builderClass, FLUENT_SETTER_METHOD_PREFIX
					+ getMethodName(method).substring(SETTER_METHOD_PREFIX_LEN));
			JVar fluentListItemsMethodParameter = fluentItemsMethod.param(collectionClass, FLUENT_SETTER_PARAMETER_NAME);
			JBlock fluentListItemsMethodBody = fluentItemsMethod.body();
			fluentListItemsMethodBody._if(jList.eq(JExpr._null()))._then().assign(jList, JExpr._new(collectionDefaulClass.narrow(collectionGeneric)));
			fluentListItemsMethodBody.invoke(jList, "addAll").arg(fluentListItemsMethodParameter);
			fluentListItemsMethodBody._return(JExpr._this());
		} else {
			generateObjectFluentMethod(method);
		}
	}
	
	protected abstract List<JClass> getMapGenerics(M method);
	
	protected void generateMapFluentMethod(M method) {
		// check if the map is a generic one
		List<JClass> mapGenerics = getMapGenerics(method);
		if (mapGenerics != null) {
			JClass mapClass = ((JClass) getMethodParameter(method)).narrow(mapGenerics);
			// builder list field definition
			JFieldVar jMapField = builderClass.field(JMod.PRIVATE, mapClass, StringUtils
					.uncapitalize(getMethodName(method).substring(SETTER_METHOD_PREFIX_LEN)));
			collectionFields.put(method, jMapField);
			// map entry fluent method
			JMethod mapEntryFluentMethod = builderClass.method(JMod.PUBLIC, builderClass, FLUENT_SETTER_METHOD_PREFIX
					+ getMethodName(method).substring(SETTER_METHOD_PREFIX_LEN));
			JVar keyParameter = mapEntryFluentMethod.param(mapGenerics.get(0), "key");
			JVar valueParameter = mapEntryFluentMethod.param(mapGenerics.get(1), FLUENT_SETTER_PARAMETER_NAME);
			JBlock mapEntryFluentMethodBody = mapEntryFluentMethod.body();
			mapEntryFluentMethodBody._if(jMapField.eq(JExpr._null()))._then().assign(jMapField, JExpr._new(mapImpl.narrow(mapGenerics)));
			mapEntryFluentMethodBody.invoke(jMapField, "put").arg(keyParameter).arg(valueParameter);
			mapEntryFluentMethodBody._return(JExpr._this());
			// map entries fluent method
			JMethod mapEntriesFluentMethod = builderClass.method(JMod.PUBLIC, builderClass, FLUENT_SETTER_METHOD_PREFIX
					+ getMethodName(method).substring(SETTER_METHOD_PREFIX_LEN));
			JVar entriesParameter = mapEntriesFluentMethod.param(mapClass, FLUENT_SETTER_PARAMETER_NAME);
			JBlock mapEntriesFluentMethodBody = mapEntriesFluentMethod.body();
			mapEntriesFluentMethodBody._if(jMapField.eq(JExpr._null()))._then().assign(jMapField, JExpr._new(mapImpl.narrow(mapGenerics)));
			mapEntriesFluentMethodBody.invoke(jMapField, "putAll").arg(entriesParameter);
			mapEntriesFluentMethodBody._return(JExpr._this());
		} else {
			generateObjectFluentMethod(method);
		}
	}
	
	protected abstract List<M> getMethods();
	
	protected abstract int getMethodParameterLength(M method);
	
	protected abstract boolean isVoid(M method);
	
	protected abstract boolean isMethodPublic(M method);
	
	/**
	 * @param target
	 *            the Class to check, may be null
	 * @param test
	 *            the Class to try to assign into, returns false if null
	 * @return <code>true</code> if assignment possible
	 */
	private boolean isAssignableFrom(JClass jClass, JClass test) {
		if (jClass.equals(test)) {
			return true;
		}
		Iterator<JClass> interfaces = jClass._implements();
		JClass current;
		while (interfaces.hasNext()) {
			current = interfaces.next();
			if (current.equals(test)) {
				return true;
			}
		}
		return false;
	}
	
	protected void generateFluentMethods() {
		for (M method : getMethods()) {
			String name = getMethodName(method);
			if (isMethodPublic(method) && //
					name.startsWith(SETTER_METHOD_PREFIX) && //
					name.length() > SETTER_METHOD_PREFIX_LEN && // 
					isVoid(method) && //
					getMethodParameterLength(method) == 1) {
				JType parameterType = getMethodParameter(method);
				if (parameterType instanceof JClass) {
					JClass parameterClass = (JClass) parameterType;
					if (isAssignableFrom(parameterClass, list)) {
						generateCollectionFluentMethod(method, listImpl);
					} else if (isAssignableFrom(parameterClass, set)) {
						generateCollectionFluentMethod(method, setImpl);
					} else if (isAssignableFrom(parameterClass, map)) {
						generateMapFluentMethod(method);
					} else {
						generateObjectFluentMethod(method);
					}
				} else {
					generateObjectFluentMethod(method);
				}
			}
		}
	}
	
	public final void generate() throws JClassAlreadyExistsException {
		// generate constructors
		generateBuilderDefaultConstructor();
		generateBuilderConstructors();
		// generate fluent methods
		generateFluentMethods();
		// generate builder pattern method
		generateBuildMethod();
	}
}
